/**
 * Copyright (c) Huawei Technologies Co., Ltd. 2019-2021. All rights reserved.
 * Description:
 */

#include "plugins/ecmascript/runtime/base/builtins_base.h"
#include "plugins/ecmascript/runtime/global_env.h"
#include "plugins/ecmascript/runtime/internal_call_params.h"
#include "plugins/ecmascript/runtime/js_promise.h"
#include "plugins/ecmascript/runtime/js_iterator.h"
#include "plugins/ecmascript/runtime/js_async_from_sync_iterator_object.h"

namespace ark::ecmascript::builtins {
// 27.1.4.2.2 %AsyncFromSyncIteratorPrototype%.next(value)
JSTaggedValue builtins::async_from_sync_iterator::proto::Next(EcmaRuntimeCallInfo *argv)
{
    BUILTINS_API_TRACE(argv->GetThread(), AsyncFromSyncIteratorPrototype, Next);

    JSThread *thread = argv->GetThread();
    JSHandle<GlobalEnv> env = thread->GetEcmaVM()->GetGlobalEnv();

    [[maybe_unused]] EcmaHandleScope handleScope(thread);

    // 1. Let O be the this value.
    JSHandle<JSTaggedValue> thisObject = builtins_common::GetThis(argv);
    // 2. Assert: Type(O) is Object and O has a [[SyncIteratorRecord]] internal slot.
    JSHandle<JSAsyncFromSyncIteratorObject> asyncFromSyncIteratorObject(
        thread, JSAsyncFromSyncIteratorObject::Cast(thisObject->GetHeapObject()));

    // 4. Let syncIteratorRecord be O.[[SyncIteratorRecord]].
    JSHandle<JSTaggedValue> iterator(thread, asyncFromSyncIteratorObject->GetIterator());
    JSHandle<JSTaggedValue> nextMethod(thread, asyncFromSyncIteratorObject->GetNextMethod());

    // 3. Let promiseCapability be ! NewPromiseCapability(%Promise%).
    JSHandle<PromiseCapability> promiseCapability =
        JSPromise::NewPromiseCapability(thread, JSHandle<JSTaggedValue>::Cast(env->GetPromiseFunction()));
    RETURN_EXCEPTION_IF_ABRUPT_COMPLETION(thread);

    JSMutableHandle<JSTaggedValue> result(thread, thread->GlobalConstants()->GetHandledUndefined());

    // 5. If value is present, then
    if (argv->GetArgsNumber() > 0) {
        // a. Let result be IteratorNext(syncIteratorRecord, value).
        result.Update(JSIterator::IteratorNext(thread, iterator, nextMethod, builtins_common::GetCallArg(argv, 0)));
    } else {
        // 6. Else,
        // a. Let result be IteratorNext(syncIteratorRecord).
        result.Update(JSIterator::IteratorNext(thread, iterator, nextMethod));
    }

    // 7. IfAbruptRejectPromise(result, promiseCapability).
    if (UNLIKELY(thread->HasPendingException())) {
        result.Update(JSPromise::IfThrowGetThrowValue(thread));
    }
    RETURN_REJECT_PROMISE_IF_ABRUPT(thread, result, promiseCapability);

    // 8. Return ! AsyncFromSyncIteratorContinuation(result, promiseCapability).
    return JSAsyncFromSyncIteratorObject::AsyncFromSyncIteratorContinuation(thread, result, promiseCapability);
}

// 27.1.4.2.3 %AsyncFromSyncIteratorPrototype%.return(value)
JSTaggedValue builtins::async_from_sync_iterator::proto::Return(EcmaRuntimeCallInfo *argv)
{
    BUILTINS_API_TRACE(argv->GetThread(), AsyncFromSyncIteratorPrototype, Return);

    JSThread *thread = argv->GetThread();
    JSHandle<GlobalEnv> env = thread->GetEcmaVM()->GetGlobalEnv();
    const GlobalEnvConstants *globalConstants = thread->GlobalConstants();

    [[maybe_unused]] EcmaHandleScope handleScope(thread);

    // 1. Let O be the this value.
    JSHandle<JSTaggedValue> thisObject = builtins_common::GetThis(argv);
    // 2. Assert: Type(O) is Object and O has a [[SyncIteratorRecord]] internal slot.
    JSHandle<JSAsyncFromSyncIteratorObject> asyncFromSyncIteratorObject(
        thread, JSAsyncFromSyncIteratorObject::Cast(thisObject->GetHeapObject()));

    // 3. Let promiseCapability be ! NewPromiseCapability(%Promise%).
    JSHandle<PromiseCapability> promiseCapability =
        JSPromise::NewPromiseCapability(thread, JSHandle<JSTaggedValue>::Cast(env->GetPromiseFunction()));
    RETURN_EXCEPTION_IF_ABRUPT_COMPLETION(thread);

    // 4. Let syncIteratorRecord be O.[[SyncIteratorRecord]].
    JSHandle<JSTaggedValue> iterator(thread, asyncFromSyncIteratorObject->GetIterator());

    // 5. Let return be GetMethod(syncIterator, "return").
    JSHandle<JSTaggedValue> returnKey = globalConstants->GetHandledReturnString();
    JSHandle<JSTaggedValue> returnMethod = JSObject::GetMethod(thread, iterator, returnKey);

    // 6. IfAbruptRejectPromise(return, promiseCapability).
    RETURN_REJECT_PROMISE_IF_ABRUPT_THROWN_VALUE(thread, returnMethod, promiseCapability);

    // 7. If return is undefined, then
    if (returnMethod->IsUndefined()) {
        // a. Let iterResult be ! CreateIterResultObject(value, true).
        JSHandle<JSObject> iterResult =
            JSIterator::CreateIterResultObject(thread, builtins_common::GetCallArg(argv, 0), true);

        // b. Perform ! Call(promiseCapability.[[Resolve]], undefined, « iterResult »).
        JSHandle<JSTaggedValue> resolve(thread, promiseCapability->GetResolve());
        auto info = NewRuntimeCallInfo(thread, resolve, JSTaggedValue::Undefined(), JSTaggedValue::Undefined(), 1);
        info->SetCallArgs(iterResult);
        [[maybe_unused]] JSTaggedValue res = JSFunction::Call(info.Get());

        // c. Return promiseCapability.[[Promise]].
        return promiseCapability->GetPromise();
    }

    JSMutableHandle<JSTaggedValue> result(thread, globalConstants->GetHandledUndefined());

    {
        ScopedCallInfo info;
        // 8. If value is present, then
        if (argv->GetArgsNumber() > 0) {
            // a. Let result be Call(return, syncIterator, « value »).
            info = NewRuntimeCallInfo(thread, returnMethod, iterator, JSTaggedValue::Undefined(), 1);
            info->SetCallArgs(builtins_common::GetCallArg(argv, 0));
        } else {
            // 9. Else,
            // a. Let result be Call(return, syncIterator).
            info = NewRuntimeCallInfo(thread, returnMethod, iterator, JSTaggedValue::Undefined(), 0);
        }
        result.Update(JSFunction::Call(info.Get()));
    }

    // 10. IfAbruptRejectPromise(result, promiseCapability).
    if (UNLIKELY(thread->HasPendingException())) {
        result.Update(JSPromise::IfThrowGetThrowValue(thread));
    }
    RETURN_REJECT_PROMISE_IF_ABRUPT(thread, result, promiseCapability);

    // 11. If Type(result) is not Object, then
    if (!result->IsECMAObject()) {
        // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »)..
        ObjectFactory *factory = thread->GetEcmaVM()->GetFactory();

        JSHandle<JSObject> typeError = factory->GetJSError(ErrorType::TYPE_ERROR, "Return is not an object");
        JSHandle<JSTaggedValue> reject(thread, promiseCapability->GetReject());
        auto info = NewRuntimeCallInfo(thread, reject, JSTaggedValue::Undefined(), JSTaggedValue::Undefined(), 1);
        info->SetCallArgs(typeError);
        [[maybe_unused]] JSTaggedValue res = JSFunction::Call(info.Get());

        // b. Return promiseCapability.[[Promise]].
        return promiseCapability->GetPromise();
    }

    // 12. Return ! AsyncFromSyncIteratorContinuation(result, promiseCapability).
    return JSAsyncFromSyncIteratorObject::AsyncFromSyncIteratorContinuation(thread, result, promiseCapability);
}

// 27.1.4.2.4 %AsyncFromSyncIteratorPrototype%.throw(exception)
JSTaggedValue builtins::async_from_sync_iterator::proto::Throw(EcmaRuntimeCallInfo *argv)
{
    BUILTINS_API_TRACE(argv->GetThread(), AsyncFromSyncIteratorPrototype, Throw);

    JSThread *thread = argv->GetThread();
    ObjectFactory *factory = thread->GetEcmaVM()->GetFactory();
    JSHandle<GlobalEnv> env = thread->GetEcmaVM()->GetGlobalEnv();
    const GlobalEnvConstants *globalConstants = thread->GlobalConstants();

    [[maybe_unused]] EcmaHandleScope handleScope(thread);

    // 1. Let O be the this value.
    JSHandle<JSTaggedValue> thisObject = builtins_common::GetThis(argv);
    // 2. Assert: Type(O) is Object and O has a [[SyncIteratorRecord]] internal slot.
    JSHandle<JSAsyncFromSyncIteratorObject> asyncFromSyncIteratorObject(
        thread, JSAsyncFromSyncIteratorObject::Cast(thisObject->GetHeapObject()));

    // 3. Let promiseCapability be ! NewPromiseCapability(%Promise%).
    JSHandle<PromiseCapability> promiseCapability =
        JSPromise::NewPromiseCapability(thread, JSHandle<JSTaggedValue>::Cast(env->GetPromiseFunction()));
    RETURN_EXCEPTION_IF_ABRUPT_COMPLETION(thread);

    // 4. Let syncIteratorRecord be O.[[SyncIteratorRecord]].
    JSHandle<JSTaggedValue> iterator(thread, asyncFromSyncIteratorObject->GetIterator());

    // 5. Let return be GetMethod(syncIterator, "throw").
    JSHandle<JSTaggedValue> throwKey = globalConstants->GetHandledThrowString();
    JSHandle<JSTaggedValue> throwMethod = JSObject::GetMethod(thread, iterator, throwKey);

    // 6. IfAbruptRejectPromise(return, promiseCapability).
    RETURN_REJECT_PROMISE_IF_ABRUPT_THROWN_VALUE(thread, throwMethod, promiseCapability);

    // 7. If throw is undefined, then
    if (throwMethod->IsUndefined()) {
        // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « value »).
        JSHandle<JSTaggedValue> reject(thread, promiseCapability->GetReject());
        ScopedCallInfo info;
        if (argv->GetArgsNumber() > 0) {
            info = NewRuntimeCallInfo(thread, reject, JSTaggedValue::Undefined(), JSTaggedValue::Undefined(), 1);
            info->SetCallArgs(builtins_common::GetCallArg(argv, 0));
        } else {
            info = NewRuntimeCallInfo(thread, reject, JSTaggedValue::Undefined(), JSTaggedValue::Undefined(), 0);
        }
        [[maybe_unused]] JSTaggedValue callResult = JSFunction::Call(info.Get());

        // b. Return promiseCapability.[[Promise]].
        return promiseCapability->GetPromise();
    }

    JSMutableHandle<JSTaggedValue> result(thread, globalConstants->GetHandledUndefined());

    {
        ScopedCallInfo info;
        // 8. If value is present, then
        if (argv->GetArgsNumber() > 0) {
            // a. Let result be Call(throw, syncIterator, « value »).
            info = NewRuntimeCallInfo(thread, throwMethod, iterator, JSTaggedValue::Undefined(), 1);
            info->SetCallArgs(builtins_common::GetCallArg(argv, 0));
        } else {
            // 9. Else,
            // a. Let result be Call(throw, syncIterator).
            info = NewRuntimeCallInfo(thread, throwMethod, iterator, JSTaggedValue::Undefined(), 0);
        }
        result.Update(JSFunction::Call(info.Get()));
    }

    // 10. IfAbruptRejectPromise(result, promiseCapability).
    if (UNLIKELY(thread->HasPendingException())) {
        result.Update(JSPromise::IfThrowGetThrowValue(thread));
    }
    RETURN_REJECT_PROMISE_IF_ABRUPT(thread, result, promiseCapability);

    // 11. If Type(result) is not Object, then
    if (!result->IsECMAObject()) {
        // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »)..
        JSHandle<JSObject> typeError = factory->GetJSError(ErrorType::TYPE_ERROR, "Throw is not an object");
        JSHandle<JSTaggedValue> reject(thread, promiseCapability->GetReject());
        auto info = NewRuntimeCallInfo(thread, reject, JSTaggedValue::Undefined(), JSTaggedValue::Undefined(), 1);
        info->SetCallArgs(typeError);
        [[maybe_unused]] JSTaggedValue callResult = JSFunction::Call(info.Get());
        // b. Return promiseCapability.[[Promise]].
        return promiseCapability->GetPromise();
    }

    // 12. Return ! AsyncFromSyncIteratorContinuation(result, promiseCapability).
    return JSAsyncFromSyncIteratorObject::AsyncFromSyncIteratorContinuation(thread, result, promiseCapability);
}
}  // namespace ark::ecmascript::builtins
